#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2024-2025  CEA, EDF
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
#

# -ex bt
# -x script.gdb
# -nx to skip .gdbinit

def defaultGDBExecutionStrRepresentation(returncode, stdout, stderr):
  return f"""returnCode = {returncode}
stdout = {stdout.decode()}
stderr = {stderr.decode()}
"""

def dealWithBacktraceAllThreads(remoteGlbs, pid, outFileStoringBt):
  idiotPattern = "@@@@"

  # voluntarary do not decrement len by one just to be sure to not forget one thread
  gdbFileForNbThreads = f"""i threads
python print("{idiotPattern}"+str( len(gdb.execute("i threads", to_string=True).splitlines())) )
"""

  gdbFileForBtOfThread = """thread {}
bt
"""

  def RetrieveNbThreadsFromOutput( output ):
    import re
    pat = re.compile( "^{}([\d]+)$".format(idiotPattern) )
    f = [elt for elt in output.split("\n") if pat.match(elt)]
    if len(f) != 1:
      raise RuntimeError(f"Fail to detect nb Threads in process with PID = {pid}")
    nbThreads = int( pat.match(f[0]).group(1) )
    return nbThreads

  def RetrieveTraceOfThread(remoteGlbs, pid, threadIdInGdbNumberingFrmt, outFileStoringBt):
    print(f"storing bt of thread {threadIdInGdbNumberingFrmt} into file {outFileStoringBt}")
    with tempfile.NamedTemporaryFile(prefix=f"thread_{threadIdInGdbNumberingFrmt}_",suffix=".gdb",mode="w") as f:
      f.write( gdbFileForBtOfThread.format(threadIdInGdbNumberingFrmt) )
      f.flush()
      returncode, stdout, stderr = remoteGlbs.execute(["gdb","-batch","-x",f"{f.name}","attach",str(pid)])
    with open(outFileStoringBt,"a") as f:
      f.write( "{}\n{}\n{}\n".format(100*"#",f"Backtrace of thread #{threadIdInGdbNumberingFrmt}",100*"#") )
      f.write( defaultGDBExecutionStrRepresentation(returncode, stdout, stderr) )

  import tempfile
  with open(outFileStoringBt,"w") as f:
    f.write( "{}\n{}\n{}\n".format(100*"#",f"Detection of number of threads in process PID = {pid}",100*"#") )
  with tempfile.NamedTemporaryFile(prefix="nb_threads_",suffix=".gdb",mode="w") as f:
    f.write( gdbFileForNbThreads )
    f.flush()
    returncode, stdout, stderr = remoteGlbs.execute(["gdb","-batch","-x",f"{f.name}","attach",str(pid)])
  with open(outFileStoringBt,"a") as f:
    f.write( defaultGDBExecutionStrRepresentation(returncode, stdout, stderr) )
  nbThreads = RetrieveNbThreadsFromOutput( stdout.decode() )
  print(f"Nb threads detected in process with PID {pid} : {nbThreads}")
  with open(outFileStoringBt,"a") as f:
    f.write( "{}\n{}\n{}\n".format(100*"#",f"Number of threads detected = {nbThreads}",100*"#") )
  for threadId in range(1,nbThreads+1):
    RetrieveTraceOfThread(remoteGlbs, pid, threadId, outFileStoringBt)
  pass

def dealStandard(remoteGlbs, pid, gdbfile):
  gdbfile = gdbfile.absolute()
  if not gdbfile.exists():
    raise RuntimeError(f"GDB commands file {gdbfile} does not exist !")
  print(f"PID tracked : {pid}")
  print(f"GDB file : {gdbfile}")
  returncode, stdout, stderr = remoteGlbs.execute(["gdb","-batch","-x",f"{gdbfile}","attach",str(pid)])
  st = defaultGDBExecutionStrRepresentation(returncode, stdout, stderr)
  print(st)

def main():
  DFT_PID_VALUE = -1

  import argparse
  from pathlib import Path
  import salome
  salome.salome_init()
  parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description = "To be used in association of a process launched nested inside a salome_process_launcher session.")
  parser.add_argument("rendez_vous_file", type=Path, help="Rendez vous file specified in corresponding salome_process_launcher.")
  parser.add_argument("-gcf","--gdb-cmds-file", dest="gdb_cmds_file", type=Path, default=None, help="GDB commands to be executed remotely.")
  parser.add_argument("--pid", dest="pid_to_track", type=int, default=DFT_PID_VALUE, help="PID of process, the debugger will be attached on ( typically a son or a little son process of process whose PID is registred inside salome_process_launcher process)")
  parser.add_argument("-abt","--backtrace-all-threads", dest ="backtrace_all_threads", default=None, help = "Specify backtrace log file to be written that will contain backtrace of all threads of target process. If activated gdb_cmds_file is ignored.", type=Path) # action='store_true'
  args = parser.parse_args()
  rdv, gdbfile = args.rendez_vous_file, args.gdb_cmds_file
  if not rdv.exists():
    raise RuntimeError(f"Rendez-vous file {rdv} does not exist !")
  if args.backtrace_all_threads and args.gdb_cmds_file:
    parser.error("Argument gdb_cmds_file is not required when --backtrace-all-threads is set.")
  if not args.backtrace_all_threads and not args.gdb_cmds_file:
    parser.error("Argument -gcf is required when -abt is not set.")
  remoteNS = salome.naming_service.LoadIORInFile(f"{rdv}")
  remoteGlbs = salome.orb.string_to_object( remoteNS.Resolve("PID_TO_TRACK").decode() )
  import pickle
  pidToTrack = pickle.loads( remoteGlbs.getAttr("CTX0") )["pid"]
  if args.pid_to_track != DFT_PID_VALUE:
    pidToTrack = args.pid_to_track
  if args.backtrace_all_threads:
    dealWithBacktraceAllThreads(remoteGlbs,pidToTrack,args.backtrace_all_threads)
  else:
    dealStandard(remoteGlbs,pidToTrack,gdbfile)

if __name__ == "__main__":
  main()
